// @ts-nocheck
const TICK = Symbol('tick');
const TICK_HANDLER = Symbol('tick-handler');
const ANIMATIONS = Symbol('animations');
const START_TIMES = Symbol('start-times');
const PAUSE_START = Symbol('pause-start');
const PAUSE_TIME = Symbol('pause-time');
const _raf = typeof requestAnimationFrame !== 'undefined' ? requestAnimationFrame : function (cb: Function) { return setTimeout(cb, 1000 / 60); };
const _caf = typeof cancelAnimationFrame !== 'undefined' ? cancelAnimationFrame : function (id: any) { clearTimeout(id); };

export class Timeline {
  state: string;
  constructor() {
    this.state = 'Initiated';
    this[ANIMATIONS] = new Set();
    this[START_TIMES] = new Map();
  }

  start() {
    if (!(this.state === 'Initiated')) return;
    this.state = 'Started';

    const startTime = Date.now();
    this[PAUSE_TIME] = 0;
    this[TICK] = () => {
      const now = Date.now();
      for (const animation of this[ANIMATIONS]) {
        let t: number;

        if (this[START_TIMES].get(animation) < startTime) {
          t = now - startTime - animation.delay - this[PAUSE_TIME];
        }
        else {
          t = now - this[START_TIMES].get(animation) - animation.delay - this[PAUSE_TIME];
        }

        if (t > animation.duration) {
          this[ANIMATIONS].delete(animation);
          t = animation.duration;
        }
        if (t > 0) animation.run(t);
      }
      this[TICK_HANDLER] = _raf(this[TICK]);
    };
    this[TICK]();
  }

  pause() {
    if (!(this.state === 'Started')) return;
    this.state = 'Paused';

    this[PAUSE_START] = Date.now();
    _caf(this[TICK_HANDLER]);
  }

  resume() {
    if (!(this.state === 'Paused')) return;
    this.state = 'Started';

    this[PAUSE_TIME] += Date.now() - this[PAUSE_START];
    this[TICK]();
  }

  reset() {
    this.pause();
    this.state = 'Initiated';
    this[PAUSE_TIME] = 0;
    this[PAUSE_START] = 0;
    this[ANIMATIONS] = new Set();
    this[START_TIMES] = new Map();
    this[TICK_HANDLER] = null;
  }

  add(animation: any, startTime?: number) {
    if (arguments.length < 2) startTime = Date.now();
    this[ANIMATIONS].add(animation);
    this[START_TIMES].set(animation, startTime);
  }
}

export class Animation {
  startValue: number;
  endValue: number;
  duration: number;
  timingFunction: (t: number) => number;
  delay: number;
  template: (t: number) => void;
  constructor(startValue: number, endValue: number, duration: number, delay: number, timingFunction: (t: number) => number, template: (v: number) => void) {
    timingFunction = timingFunction || (v => v);
    template = template || (v => v);

    this.startValue = startValue;
    this.endValue = endValue;
    this.duration = duration;
    this.timingFunction = timingFunction;
    this.delay = delay;
    this.template = template;
  }

  run(time: number) {
    const range = this.endValue - this.startValue;
    const progress = this.timingFunction(time / this.duration);
    this.template(this.startValue + range * progress);
  }
}
